using System;
using System.IO;
using System.Threading;
using DamienG.Security.Cryptography;
using FibMatrix.IO;
using FibMatrix.Schedule;
using LZ4;
using UnityEngine;
using UnityEngine.Networking;

namespace FibMatrix
{
	[FM_Mono.IgnoreGen]
	public class ResourceDownloader : IAsyncResourceTask
	{
		// private const int FRAGMENT_SIZE = 512 * 1024;
		private const int BUFFER_SIZE = 4096;

		private enum DownloadStatus
		{
			Prepared,
			Downloading,
			ValidateData,
			Completed,
			Disposed,
		}

		private ResourceDownloader(string url, ulong size, ulong compressedSize, uint crc32, 
			string storagePath, int timeout)
		{
			
			this._url = url;
			this._size = size;
			this._compressedSize = compressedSize;
			this._crc32 = crc32;
			this._storagePath = storagePath;
			this._timeout = timeout;
		}

		private ulong _size;
		private ulong _compressedSize;
		private uint _crc32;
		private int _timeout;
		private UnityWebRequestAsyncOperation _request;
		private float _progress = 0;
		private volatile DownloadStatus _status = DownloadStatus.Prepared;
		private string _error = null;
		private string _storagePath = null;
		private ulong _lastDownloadBytes = 0ul;
		private int _freezeTotalTime = 0;
		private int _timeoutChecker;
		private ulong _existFileLength = 0ul;

		private Action<IAsyncBase> _completeCallback = null;

		public event Action<IAsyncBase> completeCallback
		{
			add
			{
				this._completeCallback += value;
			}

			remove
			{
				this._completeCallback -= value;
			}
		}

		public object context
		{
			get;

			set;
		}

		private readonly string _url;
		public string url
		{
			get
			{
				return _url;
			}
		}

		public string error
		{
			get
			{
				return _error;
			}
		}

		public string storagePath
		{
			get
			{
				return this._storagePath;
			}
		}

		public bool isCompleted
		{
			get
			{
				return this._status == DownloadStatus.Completed;
			}
		}

		public float progress
		{
			get
			{
				return (_request != null ? .8f * (_request.progress * (downloadSize - _existFileLength) + _existFileLength) / downloadSize : .8f) + _progress * .2f;
			}
		}

		public ulong downloadSize
		{
			get
			{
				return _compressedSize > 0 ? _compressedSize : _size;
			}
		}

		public ulong fileSize
		{
			get
			{
				return _size;
			}
		}

		public int priority
		{
			get
			{
				return _request != null ? _request.priority : 0;
			}

			set
			{
				if (_request != null)
				{
					_request.priority = value;
				}
			}
		}

		private bool compressed
		{
			get
			{
				return _compressedSize > 0 && _compressedSize != _size;
			}
		}

		public static ResourceDownloader StartNew(string url, ulong size, ulong compressedSize, 
			uint crc32, string storagePath, Action<IAsyncBase> callback = null, 
			object context = null, int timeout = -1, bool resumeDownload = true)
		{
			var downloader = new ResourceDownloader(url, size, compressedSize, crc32, storagePath, timeout);
			downloader._completeCallback = callback;
			downloader.context = context;
			downloader.DoStart(resumeDownload);
			return downloader;
		}

		private ulong GetDownloadedBytes(UnityWebRequest uwr)
		{
			return uwr.downloadedBytes + _existFileLength;
		}

		// Use this for initialization
		//private long _startTime = 0;
		private void DoStart(bool resumeDownload)
		{
			//		Debug.Log("Download " + _node.url + " start. time " + RealTimer.elapsedMilliseconds);

			if (this._status != DownloadStatus.Prepared)
			{
				return;
			}
			this._status = DownloadStatus.Downloading;

			resumeDownload = resumeDownload && !compressed;

			_existFileLength = 0;
			if (resumeDownload)
			{
				long size = StorageManager.GetFileSize(this._storagePath);
				if (size >= 0)
				{
					_existFileLength = (ulong)size;
				}
			}
			else
			{
				StorageManager.DeleteFile(this._storagePath);	
			}

			if (_existFileLength == downloadSize)
			{
				ReadyToValidateData(null);
				return;
			}
			
			var uwr = new UnityWebRequest(this._url, UnityWebRequest.kHttpVerbGET);
			// 为了微信小游戏保存，不能用内置的DownloadHandlerFile
			uwr.downloadHandler = new DownloadHandlerBuffer(); 
			if (resumeDownload)
			{
				//请求网络数据从第fileLength到最后的字节；
				uwr.SetRequestHeader("Range", "bytes=" + _existFileLength + "-");
			}
			_request = uwr.SendWebRequest();
			_request.completed += OnReceiveAllData;
			if (_timeout > 0)
			{
				_lastDownloadBytes = GetDownloadedBytes(uwr);
				_freezeTotalTime = 0;
				_timeoutChecker = GlobalScheduler.CallOnce(1f, OnCheckTimeout);
			}
		}

		private void OnCheckTimeout(object context)
		{
			if (_request != null && !_request.isDone)
			{
				UnityWebRequest uwr = _request.webRequest;
				if (GetDownloadedBytes(uwr) != _lastDownloadBytes)
				{
					_lastDownloadBytes = GetDownloadedBytes(uwr);
					_freezeTotalTime = 0;
					_timeoutChecker = GlobalScheduler.CallOnce(1f, OnCheckTimeout);
				}
				else
				{
					_freezeTotalTime += 1;
					if (_freezeTotalTime >= _timeout)
					{
						_request.completed -= OnReceiveAllData;
						uwr.Abort();
						_request = null;
						this._error = _url + " Download timeout, freeze bytes : " + uwr.downloadedBytes + " exist file bytes " + _existFileLength + " total bytes: " + downloadSize;
						OnDownloadComplete();
					}
					else
					{
						_timeoutChecker = GlobalScheduler.CallOnce(1f, OnCheckTimeout);
					}
				}
			}
		}

		private void ReadyToValidateData(byte[] data)
		{
			this._status = DownloadStatus.ValidateData;
			//Debug.Log(this._url + " Cost " + (RealTimer.elapsedMilliseconds - this._startTime) + " ms");

#if UNITY_WEBGL
			OnWriteToDisk(data);
#else
			if (data != null)
			{
				ThreadPool.QueueUserWorkItem(OnWriteToDisk, data);
			}
			else
			{
				ThreadPool.QueueUserWorkItem(OnWriteToDisk);
			}
#endif
		}
		
		private void OnReceiveAllData(AsyncOperation op)
		{
			// Debug.Log($"downloader receiveDallData:{_url}");
			GlobalScheduler.RemoveLimitUpdator(ref _timeoutChecker);
			op.completed -= OnReceiveAllData;
			if (this._status >= DownloadStatus.Completed || _request == null || _request != op)
			{
				return;
			}
			var uwr = _request.webRequest;
			_request = null;
			if (!uwr.isNetworkError && !uwr.isHttpError)
			{
				if (GetDownloadedBytes(uwr) == downloadSize)
				{
					ReadyToValidateData(uwr.downloadHandler.data);
				}
				else
				{
					this._error = _url + " Download size validate failed! Original Size: " + downloadSize + " Download Size: " + uwr.downloadedBytes + " Exist File Size " + _existFileLength;
					this.OnDownloadComplete();
				}
			}
			else
			{
				this._error = _url + " Download error, ResponseCode: " + uwr.responseCode + " ErrorMsg: " + uwr.error;
				this.OnDownloadComplete();
			}
		}

		private void OnWriteToDisk(object param)
		{
			byte[] memoryContent = param as byte[];
			_progress = 0f;

			Crc32.PerByteCrc32 crc32 = null;
			long finalSize = 0;
			LZ4Stream ucs = null;
			Stream fs = null;
			try
			{
				if (compressed)
				{
					if (memoryContent == null)
					{
						throw new ArgumentNullException("memoryContent");
					}
					fs = StorageManager.OpenWrite(this._storagePath);
					fs.SetLength(0);
					ucs = new LZ4Stream(new MemoryStream(memoryContent), LZ4StreamMode.Decompress);
					byte[] buffer = new byte[BUFFER_SIZE];
					int readBytes = 0;
					while ((readBytes = ucs.Read(buffer, 0, BUFFER_SIZE)) > 0 && this._status != DownloadStatus.Disposed)
					{
						finalSize += readBytes;
						_progress = fileSize > 0 ? (float)finalSize / fileSize : 0;
						fs.Write(buffer, 0, readBytes);
					}
					fs.Flush();
				}
				else
				{
					crc32 = new Crc32.PerByteCrc32();
					if (memoryContent != null)
					{
						// 新的下载
						StorageManager.CreateDirectoryIfNotExist(_storagePath);
						StorageManager.WriteAllBytes(_storagePath, memoryContent);
						finalSize = memoryContent.Length;
						crc32.ComputeNextRangeBytes(memoryContent, 0, memoryContent.Length);
					}
					else
					{
						// 之前文件存在
						fs = new FileStream(this._storagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
						byte[] buffer = new byte[BUFFER_SIZE];
						int readBytes = 0;
						while ((readBytes = fs.Read(buffer, 0, BUFFER_SIZE)) > 0 && this._status != DownloadStatus.Disposed)
						{
							finalSize += readBytes;
							_progress = fileSize > 0 ? (float)finalSize / fileSize : 0;
							crc32.ComputeNextRangeBytes(buffer, 0, readBytes);
						}
					}
				}
				
				
			}
			catch (Exception e)
			{
				Debug.LogException(e);
				_error = e.ToString();
			}
			finally
			{
				if (ucs != null)
				{
					ucs.Dispose();
				}
				if (fs != null)
				{
					fs.Dispose();
				}
			}

			bool sizeValid = finalSize > 0 && (ulong) finalSize == fileSize;

			if (!sizeValid || (_crc32 != 0 && crc32 != null && crc32.GetCurrentCrc32() != _crc32))
			{
				Debug.LogError($"crc32 failed:{_url}");
				try
				{
					StorageManager.DeleteFile(_storagePath);
				}
				catch (Exception e)
				{
					Debug.LogException(e);
				}

				if (!sizeValid)
				{
					_error = _storagePath + " Crc32 valid failed";
				}
			}
			//		Debug.Log("Write " + _node.url + " complete. time " + RealTimer.elapsedMilliseconds);
			GlobalScheduler.RunToMainThread(this.OnDownloadComplete);
		}

		private void OnDownloadComplete()
		{
			if (this._status < DownloadStatus.Completed)
			{
				this._status = DownloadStatus.Completed;
				this._progress = 1f;
			}
			Action<IAsyncBase> action = this._completeCallback;
			this._completeCallback = null;
			if (action != null)
			{
				action(this);
			}
		}

		public void Dispose()
		{
			this.context = null;
			this._completeCallback = null;
			if (_request != null)
			{
				_request.completed -= OnReceiveAllData;
				_request.webRequest.Abort();
				_request = null;
			}
			_status = DownloadStatus.Disposed;
		}

	}
}
